﻿//------------------------------------------------------------------------------
// Copyright © FRA & FV 2014
// All rights reserved
//------------------------------------------------------------------------------
// Binary Serialization SDK
//
// SVN revision information:
//   $Revision: 96 $
//------------------------------------------------------------------------------
using System;
using System.IO;

namespace FRAFV.Binary.Serialization
{
	/// <summary>
	/// Adds a bounded layer to read and write operations on another stream
	/// </summary>
	[System.Diagnostics.DebuggerDisplay("{stream}[{pos}..+{length}]")]
	public sealed class BoundStream : Stream
	{
		private Stream stream;
		private long pos;
		private long length;

		#region Constructors
		/// <summary>
		/// Initializes a new instance of the <see cref="T:FRAFV.Binary.Serialization.BoundStream" /> class with the
		/// specified bound start position and length.
		/// </summary>
		/// <param name="stream">The current stream.</param>
		/// <param name="position">The bound start position.</param>
		/// <param name="length">The bound length; negative value for the stream without bound.</param>
		/// <exception cref="T:System.ArgumentNullException">
		/// <paramref name="stream" /> is null.
		/// </exception>
		/// <exception cref="T:System.ArgumentOutOfRangeException">
		/// <paramref name="position" /> is negative.
		/// </exception>
		public BoundStream(Stream stream, long position, long length)
		{
			if (stream == null)
				throw new ArgumentNullException("stream");
			if (position < 0)
				throw new ArgumentOutOfRangeException("position");
			this.stream = stream;
			this.pos = position;
			this.length = length;
		}

		/// <summary>
		/// Initializes a new instance of the <see cref="T:FRAFV.Binary.Serialization.BoundStream" /> class with the
		/// specified bound length.
		/// </summary>
		/// <param name="stream">The current stream.</param>
		/// <param name="length">The bound length; negative value for the stream without bound.</param>
		/// <exception cref="T:System.ArgumentNullException">
		/// <paramref name="stream" /> is null.
		/// </exception>
		/// <exception cref="T:System.NotSupportedException">
		/// <paramref name="stream" /> does not support seeking.
		/// </exception>
		public BoundStream(Stream stream, long length = -1)
		{
			if (stream == null)
				throw new ArgumentNullException("stream");
			this.stream = stream;
			this.pos = stream.Position;
			this.length = length;
		}
		#endregion

		#region Properties
		/// <summary>
		/// Gets a value indicating whether the current stream supports reading.
		/// </summary>
		/// <returns>
		/// <see keyword="true"/> if the stream supports reading; <see keyword="false"/> if the stream is closed or
		/// was opened with write-only access.
		/// </returns>
		public override bool CanRead
		{
			get { return stream != null && stream.CanRead; }
		}

		/// <summary>
		/// Gets a value indicating whether the current stream supports seeking.
		/// </summary>
		/// <returns>
		/// <see keyword="true"/> if the stream supports seeking; <see keyword="false"/> if the stream is closed or
		/// if the stream was constructed from an operating system handle such as a pipe or output to the console.
		/// </returns>
		public override bool CanSeek
		{
			get { return stream != null && stream.CanSeek; }
		}

		/// <summary>
		/// Gets a value indicating whether the current stream supports writing.
		/// </summary>
		/// <returns>
		/// <see keyword="true"/> if the stream supports writing; <see keyword="false"/> if the stream is closed or was
		/// opened with read-only access.
		/// </returns>
		public override bool CanWrite
		{
			get { return stream != null && stream.CanWrite; }
		}

		/// <summary>
		/// Gets the stream bound length in bytes or the length in bytes of the unbounded stream.
		/// </summary>
		/// <returns>
		/// The stream bound length in bytes or the length in bytes of the unbounded stream.
		/// </returns>
		/// <exception cref="T:System.ObjectDisposedException">
		/// Methods were called after the stream was closed.
		/// </exception>
		public override long Length
		{
			get
			{
				EnsureNotClosed();
				return length < 0L ? stream.Length : length;
			}
		}

		/// <summary>
		/// Gets the position within the current stream bound.
		/// </summary>
		/// <returns>
		/// The position within the current stream bound.
		/// </returns>
		/// <exception cref="T:System.ArgumentOutOfRangeException">
		/// The value passed is negative or is out of the bound.
		/// </exception>
		/// <exception cref="T:System.IO.IOException">
		/// An I/O error occurs, such as the stream being closed.
		/// </exception>
		/// <exception cref="T:System.NotSupportedException">
		/// The stream does not support seeking.
		/// </exception>
		/// <exception cref="T:System.ObjectDisposedException">
		/// Methods were called after the stream was closed.
		/// </exception>
		public override long Position
		{
			get
			{
				EnsureNotClosed();
				return pos;
			}
			set
			{
				if (value < 0L)
					throw new ArgumentOutOfRangeException("value");
				EnsureNotClosed();
				EnsureCanSeek();
				long delta = stream.Position - pos;
				if (value < delta || length >= 0 && value > delta + length)
					throw new ArgumentOutOfRangeException("value");
				stream.Seek(value + delta, SeekOrigin.Begin);
				pos = value;
			}
		}
		#endregion

		#region Methods
		private void EnsureCanRead()
		{
			if (!stream.CanRead)
				throw new NotSupportedException();
		}

		private void EnsureCanSeek()
		{
			if (!stream.CanSeek)
				throw new NotSupportedException();
		}

		private void EnsureCanWrite()
		{
			if (!stream.CanWrite)
				throw new NotSupportedException();
		}

		private void EnsureNotClosed()
		{
			if (this.stream == null)
				throw new ObjectDisposedException(null);
		}

		/// <summary>
		/// Releases the unmanaged resources used by the stream and optionally releases the managed resources.
		/// </summary>
		/// <param name="disposing">
		/// <see keyword="true"/> to release both managed and unmanaged resources; <see keyword="false"/> to release
		/// only unmanaged resources.</param>
		protected override void Dispose(bool disposing)
		{
			try
			{
				if (disposing && stream != null)
					stream.Close();
			}
			finally
			{
				stream = null;
				pos = 0;
				length = -1;
			}
		}

		/// <summary>
		/// Clears all buffers for the stream and causes any buffered data to be written to the underlying device.
		/// </summary>
		/// <exception cref="T:System.ObjectDisposedException">
		/// The stream has been disposed.
		/// </exception>
		/// <exception cref="T:System.IO.IOException">
		/// The data source or repository is not open.
		/// </exception>
		public override void Flush()
		{
			EnsureNotClosed();
			stream.Flush();
		}

		/// <summary>
		/// Copies bytes from the current bounded stream to an array.
		/// </summary>
		/// <returns>
		/// The total number of bytes read into <paramref name="array" />. This can be less than the number of bytes
		/// requested if that many bytes are not currently available, or <c>0</c> if the end of the stream has been
		/// reached before any data can be read.
		/// </returns>
		/// <param name="array">
		/// The buffer to which bytes are to be copied.
		/// </param>
		/// <param name="offset">
		/// The byte offset in the buffer at which to begin reading bytes.
		/// </param>
		/// <param name="count">
		/// The number of bytes to be read.
		/// </param>
		/// <exception cref="T:System.ArgumentException">
		/// Length of <paramref name="array" /> minus <paramref name="offset" /> is less than <paramref name="count" />.
		/// </exception>
		/// <exception cref="T:System.ArgumentNullException">
		/// <paramref name="array" /> is null.
		/// </exception>
		/// <exception cref="T:System.ArgumentOutOfRangeException">
		/// <paramref name="offset" /> or <paramref name="count" /> is negative.
		/// </exception>
		/// <exception cref="T:System.IO.IOException">
		/// The stream is not open or is null.
		/// </exception>
		/// <exception cref="T:System.NotSupportedException">
		/// The stream does not support reading.
		/// </exception>
		/// <exception cref="T:System.ObjectDisposedException">
		/// Methods were called after the stream was closed.
		/// </exception>
		public override int Read(byte[] array, int offset, int count)
		{
			if (array == null)
				throw new ArgumentNullException("array");
			if (offset < 0)
				throw new ArgumentOutOfRangeException("offset");
			if (count < 0)
				throw new ArgumentOutOfRangeException("count");
			if (array.Length - offset < count)
				throw new ArgumentException();
			EnsureNotClosed();
			EnsureCanRead();
			int read = stream.Read(array, offset, length < 0L || count < length - pos ? count : (int)(length - pos));
			pos += read;
			return read;
		}

		/// <summary>
		/// Sets the position within the current stream bound.
		/// </summary>
		/// <returns>
		/// The new position within the current buffered stream.
		/// </returns>
		/// <param name="offset">
		/// A byte offset relative to <paramref name="origin" />.
		/// </param>
		/// <param name="origin">
		/// A value of type <see cref="T:System.IO.SeekOrigin" /> indicating the reference point from which to obtain
		/// the new position.
		/// </param>
		/// <exception cref="T:System.IO.IOException">
		/// The stream is not open or is null.
		/// </exception>
		/// <exception cref="T:System.NotSupportedException">
		/// The stream does not support seeking.
		/// </exception>
		/// <exception cref="T:System.ObjectDisposedException">
		/// Methods were called after the stream was closed.
		/// </exception>
		public override long Seek(long offset, SeekOrigin origin)
		{
			EnsureNotClosed();
			EnsureCanSeek();
			long delta = stream.Position - pos;
			switch (origin)
			{
				case SeekOrigin.Begin:
					offset += delta;
					break;
				case SeekOrigin.End:
					if (length >= 0L)
						offset += stream.Length - length;
					break;
			}
			stream.Seek(offset, origin);
			pos = stream.Position - delta;
			return pos;
		}

		/// <summary>
		/// Sets the length of the stream bound.
		/// </summary>
		/// <param name="value">
		/// An integer indicating the desired length of the current stream bound in bytes or negative value to clear
		/// the bound or the stream.
		/// </param>
		public override void SetLength(long value)
		{
			length = value;
		}

		/// <summary>
		/// Copies bytes to the bounded stream and advances the current position within the stream by the number of bytes
		/// written.
		/// </summary>
		/// <param name="array">
		/// The byte array from which to copy <paramref name="count" /> bytes to the current bounded stream.
		/// </param>
		/// <param name="offset">
		/// The offset in the buffer at which to begin copying bytes to the current bounded stream. </param>
		/// <param name="count">
		/// The number of bytes to be written to the current bounded stream.
		/// </param>
		/// <exception cref="T:System.ArgumentException">
		/// Length of <paramref name="array" /> minus <paramref name="offset" /> is less than <paramref name="count" />.
		/// </exception>
		/// <exception cref="T:System.ArgumentNullException">
		/// <paramref name="array" /> is null.
		/// </exception>
		/// <exception cref="T:System.ArgumentOutOfRangeException">
		/// <paramref name="offset" /> or <paramref name="count" /> is negative.
		/// </exception>
		/// <exception cref="T:System.IO.IOException">
		/// The stream is closed or null.
		/// </exception>
		/// <exception cref="T:System.NotSupportedException">
		/// The stream does not support writing.
		/// </exception>
		/// <exception cref="T:System.ObjectDisposedException">
		/// Methods were called after the stream was closed.
		/// </exception>
		/// <exception cref="T:System.IO.EndOfStreamException">
		/// <paramref name="count"/> is out of the bound.
		/// </exception>
		public override void Write(byte[] array, int offset, int count)
		{
			if (array == null)
				throw new ArgumentNullException("array");
			if (offset < 0)
				throw new ArgumentOutOfRangeException("offset");
			if (count < 0)
				throw new ArgumentOutOfRangeException("count");
			if (array.Length - offset < count)
				throw new ArgumentException();
			EnsureNotClosed();
			EnsureCanWrite();
			if (length >= 0L && count > length - pos)
				throw new EndOfStreamException();
			stream.Write(array, offset, count);
			pos += count;
		}
		#endregion
	}
}
